from demo.evo.problems import Problem
from eden.evo.aminos import AddAmino, MultiAmino, AssignAmino
from eden.evo.operands import RegisterOperand, ConstOperand
from eden.evo.protein import Protein

class AlgebraProblem(Problem):
    def _test_data(self):
        data = tuple(((x, ), (self.__calc_y(x), )) for x in range(10))
        return data
    
    def _format_solution_equation(self, input, output):
        expression = self.expression().replace('x', '%(x)d') + ' = %(y)d'
        equation = expression % {'x':input[0], 'y':output[0]}
        return equation

    def __calc_y(self, x):
        y = eval(self.expression(), None, {'x' : x})
        return y

class AddOneProblem(AlgebraProblem):
    '''
    Calculates y = x + 1.
    Recommended random seed: 0
    
    The cells need to evolve from Null(r0, 0) to Add(r0, 1).
    This could be simply decomposed to two steps:
    a) Null(r0, 0) --> Add(r0, 0)
    b) Add(r0, 0) --> Add(r0, 1)
    or
    a) Null(r0, 0) --> Null(r0, 1)
    b) Null(r0, 1) --> Add(r0, 1)
    '''
    
    def expression(self):
        return 'x + 1'
    
    def solution(self):
        amino = AddAmino(RegisterOperand(0), ConstOperand(1))
        return Protein((amino, ))
    
class AddTwoProblem(AlgebraProblem):
    '''
    Calculates y = x + 2.
    Recommended random seed: 0
    
    The cells need to evolve from Null(r0, 0) to Add(r0, 2).
    This could be simply decomposed to three steps:
    a) Null(r0, 0) --> Add(r0, 0)
    b) Add(r0, 0) --> Add(r0, 1)
    c) Add(r0, 1) --> Add(r0, 2)
    or
    a) Null(r0, 0) --> Null(r0, 1)
    b) Null(r0, 1) --> Add(r0, 1)
    b) Add(r0, 1) --> Add(r0, 2)
    or
    a) Null(r0, 0) --> Null(r0, 1)
    b) Null(r0, 1) --> Null(r0, 2)
    c) Null(r0, 2) --> Add(r0, 2)
    '''
    
    def expression(self):
        return 'x + 2'
    
    def solution(self):
        amino = AddAmino(RegisterOperand(0), ConstOperand(2))
        return Protein((amino, ))
            
class TimesTwoProblem(AlgebraProblem):
    '''
    Calculates y = x * 2.
    Recommended random seed: 1
    
    The cells need to evolve from Null(r0, 0) to Multi(r0, 2).
    You may think this is similar to the add-two Problem above.
    Similarly, there are three solutions:
    SOLUTION1
    a) Null(r0, 0) --> Multi(r0, 0)
    b) Multi(r0, 0) --> Multi(r0, 1)
    c) Multi(r0, 1) --> Multi(r0, 2)
    SOLUTION2
    a) Null(r0, 0) --> Null(r0, 1)
    b) Null(r0, 1) --> Multi(r0, 1)
    b) Multi(r0, 1) --> Multi(r0, 2)
    SOLUTION3
    a) Null(r0, 0) --> Null(r0, 1)
    b) Null(r0, 1) --> Null(r0, 2)
    c) Null(r0, 2) --> Multi(r0, 2)
    
    However, the fitness of Multi(r0, 0) is lower than Null(r0, 0).
    So that the evolution Null(r0, 0) --> Multi(r0, 0) is highly impossible.
    It is like that there is a potential energy barrier between Null(r0, 0) and Multi(r0, 1).
    
    There is a very small possibility that the cell can do a tunneling.
    This is equivalent to evolve from Null(r0, 0) directly to Multi(r0, 1) which has the same fitness.
    But this requires the codon to mutate two bases in a single step which is very unlikely.
    
    As a result, solution 2 & 3 are the only practical solutions.
    '''
    
    def expression(self):
        return 'x * 2'
    
    def solution(self):
        amino = MultiAmino(RegisterOperand(0), ConstOperand(2))
        return Protein((amino, ))
    
class SquaredProblem(AlgebraProblem):
    '''
    Calculates y = x * x.
    Recommended random seed: 0
    '''
    
    def expression(self):
        return 'x * x'
    
    def solution(self):
        amino = MultiAmino(RegisterOperand(0), RegisterOperand(0))
        return Protein((amino, ))
    
class LinearProblem(AlgebraProblem):
    def expression(self):
        return '2 * x + 1'
    
    def solution(self):
        amino1 = MultiAmino(RegisterOperand(0), ConstOperand(2))
        amino2 = AddAmino(RegisterOperand(0), ConstOperand(1))
        return Protein((amino1, amino2))
    
class QuadraticProblem(AlgebraProblem):
    def expression(self):
        return 'x * x + 2 * x + 1'
    
    def solution1(self):
        amino1 = AssignAmino(RegisterOperand(1), RegisterOperand(0))
        amino2 = MultiAmino(RegisterOperand(0), RegisterOperand(0))
        amino3 = MultiAmino(RegisterOperand(1), ConstOperand(2))
        amino4 = AddAmino(RegisterOperand(0), RegisterOperand(1))
        amino5 = AddAmino(RegisterOperand(0), ConstOperand(1))
        return Protein((amino1, amino2, amino3, amino4, amino5))
    
    def solution2(self):
        # the expression is equivalent to (x + 1) * (x + 1)
        amino1 = AddAmino(RegisterOperand(0), ConstOperand(1))
        amino2 = MultiAmino(RegisterOperand(0), RegisterOperand(0))
        return Protein((amino1, amino2))
    
class PhasedQuadraticProblem(AlgebraProblem):
    def solution(self):
        amino1 = AddAmino(RegisterOperand(0), ConstOperand(1))
        amino2 = MultiAmino(RegisterOperand(0), RegisterOperand(0))
        return Protein((amino1, amino2))
    
    def solve(self):
        # phase1
        self.expression = lambda: 'x + 1'
        Problem.solve(self)
        # phase2
        self.expression = lambda: '(x + 1) * (x + 1)'
        self.pool.reproducer.mutators[-1].chromosome = self.pool.cells[0].chromosome
        Problem.solve(self)